package de.mmis.core.base;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

import org.apache.log4j.Logger;

/**
 * class providing a static method to get the raw bytes of the jar file
 * containing a specified class
 * 
 * @author Christian Koch
 * 
 */
public abstract class GetJarData {

	private final static Logger LOGGER = Logger.getLogger(GetJarData.class);

	private static byte[] lastJarData;
	private static URL lastJarURL;

	/**
	 * reads the jar file, the given class was loaded from by the
	 * {@link ClassLoader}. If the location is a file on local hard drive, a jar
	 * file is created on-the-fly.
	 * 
	 * @param clazz
	 *            class whose location determines which jar / local path will be
	 *            read and returned
	 * @return the raw bytes of the jar file containing a specified class.
	 */
	public synchronized static byte[] getJarData(Class<?> clazz) {
		LOGGER.debug("getJarData: jar file data of " + clazz + " requested");

		String resourceName = clazz.getName().replace(".", "/") + ".class";

		LOGGER.debug("getJarData: search for resource " + resourceName);

		ClassLoader cl = clazz.getClassLoader();
		if (cl == null) // root class loader my be null
			cl = GetJarData.class.getClassLoader();
		URL url = cl.getResource(resourceName);

		if (url == null) {
			LOGGER.error("getJarData: cannot find resource " + resourceName);
			return null;
		}

		LOGGER.debug("getJarData: found resource at URL " + url);

		if ("file".equals(url.getProtocol())) {
			String urlString = url.toString();
			if (urlString.endsWith(resourceName)) {
				String baseURIPath = urlString.substring(0, urlString.length()
						- resourceName.length());

				LOGGER.debug("getJarData: valid local file system URL with base path "
						+ baseURIPath);

				URI baseURI;
				try {
					baseURI = new URI(baseURIPath);
				} catch (URISyntaxException e) {
					LOGGER.error(e, e);
					return null;
				}

				try {
					if (baseURI.toURL().equals(lastJarURL))
						return lastJarData;
				} catch (MalformedURLException e) {
					// ignore since its not needed
				}

				LOGGER.debug("getJarData: creating jar data for directory");

				ByteArrayOutputStream baos = new ByteArrayOutputStream();
				JarOutputStream jos;
				try {
					jos = new JarOutputStream(baos);
					add(new File(baseURI), jos, baseURIPath);
					jos.close();
				} catch (IOException e) {
					LOGGER.error(e, e);
					return null;
				}

				try {
					lastJarURL = baseURI.toURL();
					lastJarData = baos.toByteArray();

					return lastJarData;
				} catch (MalformedURLException e) {
					// ignore since its not needed
				}
				return baos.toByteArray();
			}
		}

		if (!"jar".equals(url.getProtocol())) {
			LOGGER.error("getJarData: resource URL "
					+ url
					+ " is neither a local file nor located inside a jar file, exiting");
			return null;
		}

		String jarLocation = url.getPath().substring(0,
				url.getPath().lastIndexOf("!"));
		LOGGER.debug("getJarData: jar location is " + jarLocation);

		try {
			url = new URL(jarLocation);
		} catch (MalformedURLException e) {
			LOGGER.error(e, e);
			return null;
		}

		LOGGER.debug("getJarData: jar location URL is " + url);

		if (!url.equals(lastJarURL)) {
			LOGGER.debug("getJarData: opening jar input stream...");
			InputStream in;
			try {
				in = url.openStream();
			} catch (IOException e) {
				LOGGER.error("getJarData: cannot open InputStream for jar URL",
						e);
				return null;
			}
			ByteArrayOutputStream out = new ByteArrayOutputStream();

			LOGGER.debug("getJarData: copy jar data...");
			try {
				byte[] bytes = new byte[1024];
				int len;
				while ((len = in.read(bytes)) > -1)
					out.write(bytes, 0, len);
			} catch (IOException e) {
				LOGGER.error(
						"getJarData: error while reading from jar InputStream",
						e);
				return null;
			}

			lastJarURL = url;
			lastJarData = out.toByteArray();

			LOGGER.debug("getJarData: jar data downloaded successfully. Got "
					+ lastJarData.length + " bytes of data.");

		} else
			LOGGER.debug("getJarData: already got data from there, returning old data");

		return lastJarData;
	}

	/**
	 * recursively add files and directories to new jar file
	 * 
	 * @param source
	 *            file or directory to add.
	 * @param target
	 *            jar output stream to write to
	 * @param baseURIPath
	 *            base URI path all entries will be relative to.
	 * @throws IOException
	 *             an error occurred while writing to jar.
	 */
	private static void add(File source, JarOutputStream target,
			String baseURIPath) throws IOException {
		BufferedInputStream in = null;
		try {
			String sourceName = source.toURI().toString();
			if (source.isDirectory() && !sourceName.endsWith("/"))
				sourceName += "/";
			if (!sourceName.startsWith(baseURIPath)) {
				LOGGER.warn("warning: path " + sourceName
						+ " has no base prefix " + baseURIPath
						+ " as expected. skipping");
				return;
			}
			sourceName = sourceName.substring(baseURIPath.length());

			if (source.isDirectory()) {
				if (!sourceName.isEmpty()) {
					JarEntry entry = new JarEntry(sourceName);
					target.putNextEntry(entry);
					target.closeEntry();
				}
				for (File nestedFile : source.listFiles())
					add(nestedFile, target, baseURIPath);
				return;
			}

			JarEntry entry = new JarEntry(sourceName);
			target.putNextEntry(entry);
			in = new BufferedInputStream(new FileInputStream(source));

			byte[] buffer = new byte[1024];
			int count;
			while ((count = in.read(buffer)) > -1)
				target.write(buffer, 0, count);

			target.closeEntry();
		} finally {
			if (in != null)
				in.close();
		}
	}
}
